/*
 * Copyright (c) 2008, Takashi Ito
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the authors nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include "tdom-internal.h"
#include "tdom.h"

void tdom_attr_delete(struct tdom_attr *attr)
{
    if (attr) {
        free(attr->node.name);
        free(attr->value);
        free(attr);
    }
}

void tdom_text_delete(struct tdom_text *text)
{
    if (text) {
        free(text->value);
        free(text);
    }
}

void tdom_ns_delete(struct tdom_ns *ns)
{
    if (ns) {
        free(ns->prefix);
        free(ns->uri);
        free(ns);
    }
}

void tdom_elem_delete(struct tdom_elem *elem)
{
    struct tdom_node *node;
    struct tdom_ns *ns;
    if (elem) {
        TAILQ_FREE_ALL(node, &elem->children, sibling, tdom_node_delete);
        TAILQ_FREE_ALL(node, &elem->attrs, sibling, tdom_node_delete);
        TAILQ_FREE_ALL(ns, &elem->nss, sibling, tdom_ns_delete);
        free(elem->node.name);
        free(elem);
    }
}

struct tdom_doc *tdom_doc_new(struct tdom_elem *root,
                              const char *version,
                              const char *encoding)
{
    struct tdom_doc *doc = malloc(sizeof(struct tdom_doc));
    if (doc) {
        doc->root = root;
        doc->version = NULL;
        doc->encoding = NULL;
        if (tdom_doc_set(doc, version, encoding) < 0) {
            free(doc);
            doc = NULL;
        }
    }
    return doc;
}

void tdom_doc_delete(struct tdom_doc *doc)
{
    if (doc) {
        tdom_elem_delete(doc->root);
        free(doc->version);
        free(doc->encoding);
        free(doc);
    }
}

void tdom_node_delete(struct tdom_node *node)
{
    if (node->type == TDOM_ELEM) {
        tdom_elem_delete((struct tdom_elem *)node);
    } else if (node->type == TDOM_ATTR) {
        tdom_attr_delete((struct tdom_attr *)node);
    } else if (node->type == TDOM_TEXT) {
        tdom_text_delete((struct tdom_text *)node);
    }
}

int tdom_doc_set(struct tdom_doc *doc,
                 const char *version,
                 const char *encoding)
{
    char *v = NULL;
    char *e = NULL;

    if (doc == NULL) {
        return -1;
    }

    if (version) {
        if ((v = strdup(version)) == NULL) {
            goto out;
        }
    }
    if (encoding) {
        if ((e = strdup(encoding)) == NULL) {
            goto out;
        }
    }
    free(doc->version);
    free(doc->encoding);
    doc->version = v;
    doc->encoding = e;
    return 0;

out:
    free(v);
    free(e);
    return -2;
}

static int tdom_node_init(struct tdom_node *node,
                          struct tdom_elem *parent,
                          enum tdom_node_type type,
                          const char *name)
{
    node->type = type;
    node->parent = parent;
    node->name = NULL;
    if (name) {
        if ((node->name = strdup(name)) == NULL) {
            return -2;
        }
    }
    return 0;
}

struct tdom_elem *tdom_elem_new(struct tdom_elem *parent, const char *name)
{
    struct tdom_elem *elem = malloc(sizeof(struct tdom_elem));
    if (elem) {
        TAILQ_INIT(&elem->children);
        TAILQ_INIT(&elem->attrs);
        TAILQ_INIT(&elem->nss);
        if (tdom_node_init(&elem->node, parent, TDOM_ELEM, name) != 0) {
            tdom_elem_delete(elem);
            elem = NULL;
        }
    }
    return elem;
}

struct tdom_attr *tdom_attr_new(struct tdom_elem *parent,
                                const char *name,
                                const char *value)
{
    struct tdom_attr *attr = malloc(sizeof(struct tdom_attr));
    if (attr) {
        attr->value = NULL;
        if (tdom_node_init(&attr->node, parent, TDOM_ATTR, name) != 0) {
            goto out;
        }
        if (value) {
            if ((attr->value = strdup(value)) == NULL) {
                goto out;
            }
        }
    }
    return attr;

out:
    tdom_attr_delete(attr);
    return NULL;
}

struct tdom_text *tdom_text_new(struct tdom_elem *parent, const char *value)
{
    struct tdom_text *text = malloc(sizeof(struct tdom_text));
    if (text) {
        tdom_node_init(&text->node, parent, TDOM_TEXT, NULL);
        text->value = NULL;
        if (value) {
            if ((text->value = strdup(value)) == NULL) {
                tdom_text_delete(text);
                text = NULL;
            }
        }
    }
    return text;
}

struct tdom_ns *tdom_ns_new(const char *prefix, const char *uri)
{
    struct tdom_ns *ns;
    
    if ((prefix != NULL && uri == NULL) || (prefix == NULL && uri != NULL)) {
        return NULL;
    }
    
    ns = malloc(sizeof(struct tdom_ns));
    if (ns) {
        ns->prefix = NULL;
        ns->uri = NULL;
        if (prefix) {
            if ((ns->prefix = strdup(prefix)) == NULL) {
                goto out;
            }
            if ((ns->uri = strdup(uri)) == NULL) {
                goto out;
            }
        }
    }
    return ns;

out:
    tdom_ns_delete(ns);
    return NULL;
}

void tdom_append_child(struct tdom_elem *elem, struct tdom_node *child)
{
    if (elem && child) {
        TAILQ_INSERT_TAIL(&elem->children, child, sibling);
        child->parent = elem;
    }
}

void tdom_insert_after(struct tdom_elem *elem,
                       struct tdom_node *child,
                       struct tdom_node *ref)
{
    if (elem && child) {
        if (ref) {
            TAILQ_INSERT_AFTER(&elem->children, ref, child, sibling);
        } else {
            TAILQ_INSERT_TAIL(&elem->children, child, sibling);
        }
        child->parent = elem;
    }
}

void tdom_remove_child(struct tdom_elem *elem, struct tdom_node *child)
{
    if (elem && child) {
        TAILQ_REMOVE(&elem->children, child, sibling);
        child->parent = NULL;
    }
}

static int tdom_create_prefix(struct tdom_elem *elem, const char *ns_uri, char *buff, int size)
{
    int i;
    const char *uri;

    tdom_assert(elem);
    tdom_assert(buff);
    tdom_assert(size >= 8);

    for (i = 0; i <= TDOM_GEN_PREFIX_MAX; i++) {
        snprintf(buff, size, "ns%d", i);
        uri = tdom_lookup_ns_uri(elem, buff, strlen(buff));
        if (uri == NULL) {
            return 0;
        }
    }

    return -1;
}

int tdom_set_elem_value(struct tdom_elem *elem,
                        const char *name,
                        const char *value,
                        const char *ns_uri)
{
    struct tdom_elem *target;
    const char *prefix = NULL;
    char p[8];
    char *qname = NULL;
    int size;

    if ((target = tdom_get_elem(elem, name, ns_uri, 0))) {
        return tdom_set_value(target, value);
    }

    if (elem == NULL || name == NULL || value == NULL) {
        return -1;
    }

    if (ns_uri) {
        if ((prefix = tdom_lookup_ns_prefix(elem, ns_uri, 0)) == NULL) {
            if (tdom_create_prefix(elem, ns_uri, p, sizeof(p)) != 0) {
                return -3;
            }
            prefix = p;
        }
    }

    if (prefix) {
        size = strlen(prefix) + strlen(name) + 2;
        if ((qname = malloc(size)) == NULL) {
            return -2;
        }
        snprintf(qname, size, "%s:%s", prefix, name);
        name = qname;
    }

    target = tdom_elem_new(NULL, name);
    free(qname);
    if (target == NULL) {
        return -2;
    }

    if (prefix) {
        if (tdom_set_ns(target, prefix, ns_uri) != 0) {
            goto out;
        }
    }

    if (tdom_set_value(target, value) == 0) {
        tdom_append_child(elem, &target->node);
        return 0;
    }

out:
    tdom_elem_delete(target);
    return -2;
}

static
struct tdom_attr *tdom_get_attr_node(struct tdom_elem *elem,
                                     const char *name,
                                     const char *ns_uri)
{
    struct tdom_node *node;
    const char *end = NULL;
    int len;
    const char *uri;

    if (elem == NULL || name == NULL) {
        return NULL;
    }

    if ((ns_uri && tdom_is_ns_default(elem, ns_uri)) ||
        (ns_uri == NULL && (end = strchr(name, ':')) == NULL)) {
        TAILQ_FOREACH(node, &elem->attrs, sibling) {
            tdom_assert(node->type == TDOM_ATTR);
            if (strcmp(node->name, name) == 0) {
                return (struct tdom_attr *)node;
            }
        }
        if (ns_uri) {
            end = strchr(name, ':');
        } else {
            return NULL;
        }
    }

    if (end != NULL) {
        if ((ns_uri = tdom_lookup_ns_uri(elem, name, end - name)) == NULL) {
            return NULL;
        }
        name = end + 1;
    }

    TAILQ_FOREACH(node, &elem->attrs, sibling) {
        tdom_assert(node->type == TDOM_ATTR);
        if ((end = strchr(node->name, ':')) == NULL) {
            continue;
        }
        if (strcmp(name, end + 1) != 0) {
            continue;
        }
        len = end - node->name;
        if ((uri = tdom_lookup_ns_uri(elem, node->name, len)) == NULL) {
            continue;
        }
        if (strcasecmp(uri, ns_uri) == 0) {
            return (struct tdom_attr *)node;
        }
    }

    return NULL;
}

const char *tdom_get_attr(const struct tdom_elem *elem,
                          const char *name,
                          const char *ns_uri)
{
    struct tdom_attr *attr = tdom_get_attr_node((struct tdom_elem *)elem, name, ns_uri);
    return attr ? attr->value : NULL;
}

int tdom_set_attr(struct tdom_elem *elem,
                  const char *name,
                  const char *value,
                  const char *ns_uri)
{
    struct tdom_attr *attr;
    const char *prefix;
    char p[8];
    char *qname;
    int size;

    if (elem == NULL || name == NULL || value == NULL) {
        return -1;
    }

    if ((attr = tdom_get_attr_node(elem, name, ns_uri))) {
        if ((qname = strdup(value)) == NULL) {
            return -2;
        }
        free(attr->value);
        attr->value = qname;
        return 0;
    }

    if (ns_uri == NULL || tdom_is_ns_default(elem, ns_uri)) {
        if ((attr = tdom_attr_new(elem, name, value)) == NULL) {
            return -2;
        }
        TAILQ_INSERT_TAIL(&elem->attrs, &attr->node, sibling);
        return 0;
    }

    if ((prefix = tdom_lookup_ns_prefix(elem, ns_uri, 0)) == NULL) {
        if (tdom_create_prefix(elem, ns_uri, p, sizeof(p)) != 0) {
            return -3;
        }
        if (tdom_set_ns(elem, p, ns_uri) != 0) {
            return -2;
        }
        prefix = p;
    }

    size = strlen(prefix) + strlen(name) + 2;
    if ((qname = malloc(size)) == NULL) {
        return -2;
    }
    snprintf(qname, size, "%s:%s", prefix, name);
    if ((attr = tdom_attr_new(elem, qname, value)) == NULL) {
        free(qname);
        return -2;
    }

    free(qname);
    TAILQ_INSERT_TAIL(&elem->attrs, &attr->node, sibling);
    return 0;
}

void tdom_remove_attr(struct tdom_elem *elem,
                      const char *name,
                      const char *ns_uri)
{
    struct tdom_attr *attr = tdom_get_attr_node(elem, name, ns_uri);
    if (attr) {
        TAILQ_REMOVE(&elem->attrs, &attr->node, sibling);
        tdom_attr_delete(attr);
    }
}

int tdom_set_ns(struct tdom_elem *elem, const char *prefix, const char *uri)
{
    struct tdom_ns *ns;

    if (elem == NULL || prefix == NULL || uri == NULL) {
        return -1;
    }

    TAILQ_FOREACH(ns, &elem->nss, sibling) {
        if (strcmp(ns->prefix, prefix) == 0) {
            return 1;
        }
    }

    ns = tdom_ns_new(prefix, uri);
    if (ns == NULL) {
        return -2;
    }

    TAILQ_INSERT_TAIL(&elem->nss, ns, sibling);
    return 0;
}

struct tdom_elem *tdom_get_elem(struct tdom_elem *elem,
                                const char *name,
                                const char *ns_uri,
                                int index)
{
    struct tdom_node *node;
    struct tdom_elem *current;
    const char *end;
    int len;
    const char *uri;

    if (elem == NULL || name == NULL || index < 0) {
        return NULL;
    }

    if (ns_uri == NULL) {
        TAILQ_FOREACH(node, &elem->children, sibling) {
            if (node->type == TDOM_ELEM) {
                tdom_assert(node->name);
                if (strcmp(node->name, name) == 0) {
                    if (index <= 0) {
                        return (struct tdom_elem*)node;
                    }
                    index--;
                }
            }
        }
        return NULL;
    }

    TAILQ_FOREACH(node, &elem->children, sibling) {
        if (node->type != TDOM_ELEM) {
            continue;
        }
        tdom_assert(node->name);

        current = (struct tdom_elem *)node;
        if (tdom_is_ns_default(current, ns_uri)) {
            if (strcmp(node->name, name) == 0) {
                if (index <= 0) {
                    return current;
                }
                index--;
                continue;
            }
        }

        if ((end = strchr(node->name, ':')) == NULL) {
            continue;
        }
        if (strcmp(name, end + 1) != 0) {
            continue;
        }
        len = end - node->name;
        if ((uri = tdom_lookup_ns_uri(current, node->name, len)) == NULL) {
            continue;
        }
        if (strcasecmp(uri, ns_uri) == 0) {
            if (index <= 0) {
                return current;
            }
            index--;
            continue;
        }
    }

    return NULL;
}

int tdom_count_elem(const struct tdom_elem *elem,
                    const char *name,
                    const char *ns_uri)
{
    int count = 0;
    struct tdom_node *node;
    struct tdom_elem *current;
    const char *end;
    int len;
    const char *uri;

    if (elem == NULL || name == NULL) {
        return 0;
    }

    if (ns_uri == NULL) {
        TAILQ_FOREACH(node, &elem->children, sibling) {
            if (node->type == TDOM_ELEM) {
                tdom_assert(node->name);
                if (strcmp(node->name, name) == 0) {
                    count++;
                }
            }
        }
        return count;
    }

    TAILQ_FOREACH(node, &elem->children, sibling) {
        if (node->type != TDOM_ELEM) {
            continue;
        }
        tdom_assert(node->name);

        current = (struct tdom_elem *)node;
        if (tdom_is_ns_default(current, ns_uri)) {
            if (strcmp(node->name, name) == 0) {
                count++;
                continue;
            }
        }

        if ((end = strchr(node->name, ':')) == NULL) {
            continue;
        }
        if (strcmp(name, end + 1) != 0) {
            continue;
        }
        len = end - node->name;
        if ((uri = tdom_lookup_ns_uri(current, node->name, len)) == NULL) {
            continue;
        }
        if (strcasecmp(uri, ns_uri) == 0) {
            count++;
            continue;
        }
    }

    return count;
}

const char *tdom_get_value(const struct tdom_elem *elem)
{
    struct tdom_node *node;

    if (elem == NULL) {
        return NULL;
    }

    TAILQ_FOREACH(node, &elem->children, sibling) {
        if (node->type == TDOM_TEXT) {
            return ((struct tdom_text *)node)->value;
        }
    }

    return NULL;
}

int tdom_set_value(struct tdom_elem *elem, const char *value)
{
    struct tdom_node *node;
    struct tdom_text *text = NULL;
    char *newval;

    if (elem == NULL || value == NULL) {
        return -1;
    }

    TAILQ_FOREACH(node, &elem->children, sibling) {
        if (node->type == TDOM_TEXT) {
            text = (struct tdom_text *)node;
            break;
        }
    }

    if (text) {
        if ((newval = strdup(value)) == NULL) {
            return -2;
        }
        free(text->value);
        text->value = newval;
    } else {
        if ((text = tdom_text_new(NULL, value)) == NULL) {
            return -2;
        }
        tdom_append_child(elem, &text->node);
    }

    return 0;
}

const char *tdom_lookup_ns_uri(const struct tdom_elem *elem, const char *prefix, int len)
{
    const struct tdom_elem *current;
    struct tdom_ns *ns;
    const char *uri;

    if (elem == NULL || prefix == NULL || len < 0) {
        return NULL;
    }

    TAILQ_FOREACH(ns, &elem->nss, sibling) {
        if (strncmp(ns->prefix, prefix, len) == 0 && ns->prefix[len] == '\0') {
            return ns->uri;
        }
    }
    current = elem;
    while (current != NULL) {
        if ((uri = tdom_lookup_ns_uri(current->node.parent, prefix, len))) {
            return uri;
        }
        current = current->node.parent;
    }

    return NULL;
}

const char *tdom_lookup_ns_prefix(const struct tdom_elem *elem,
                                  const char *uri,
                                  int index)
{
    const struct tdom_elem *current;
    struct tdom_ns *ns;
    const char *prefix;
    const char *expected;

    if (elem == NULL || uri == NULL || index < 0) {
        return NULL;
    }

    TAILQ_FOREACH(ns, &elem->nss, sibling) {
        if (strcasecmp(ns->uri, uri) == 0) {
            if (index <= 0) {
                return ns->prefix;
            }
            index--;
        }
    }
    current = elem;
    while (current != NULL) {
        if ((prefix = tdom_lookup_ns_prefix(current->node.parent, uri, index))) {
            expected = tdom_lookup_ns_uri(elem, prefix, strlen(prefix));
            tdom_assert(expected);
            if (expected && (strcasecmp(uri, expected) == 0)) {
                return prefix;
            }
        }
        current = current->node.parent;
    }

    return NULL;
}

int tdom_is_ns_default(const struct tdom_elem *elem, const char *uri)
{
    struct tdom_ns *ns;

    if (uri == NULL) {
        return 0;
    }

    while (elem != NULL) {
        TAILQ_FOREACH(ns, &elem->nss, sibling) {
            if (*ns->prefix == '\0') {
                return (strcasecmp(ns->uri, uri) == 0) ? 1 : 0;
            }
        }
        elem = elem->node.parent;
    }

    return 0;
}

int tdom_count_ns_prefix(const struct tdom_elem *elem, const char *uri)
{
    int ret = 0;
    struct tdom_ns *ns;
    const struct tdom_elem *current;
    const char *expected;

    if (uri == NULL) {
        return 0;
    }

    current = elem;
    while (current != NULL) {
        TAILQ_FOREACH(ns, &current->nss, sibling) {
            if (strcasecmp(ns->uri, uri) == 0) {
                expected = tdom_lookup_ns_uri(elem, ns->prefix, strlen(ns->prefix));
                if (strcasecmp(expected, uri) == 0) {
                    ret++;
                }
            }
        }
        current = current->node.parent;
    }

    return ret;
}

const char *ignore_space(const char *s, int *len)
{
    tdom_assert(s != NULL);
    tdom_assert(len != NULL);
    tdom_assert((*len) >= 0);

    if ((s = skip_head_space(s, len))) {
        return skip_tail_space(s, len);
    }

    return NULL;
}

const char *skip_head_space(const char *s, int *len)
{
    int i;

    tdom_assert(s != NULL);
    tdom_assert(len != NULL);
    tdom_assert((*len) >= 0);

    for (i = 0; i < (*len); i++) {
        if (TDOM_IS_SPACE(s[i]) || TDOM_IS_CRLF(s[i])) {
            continue;
        }
        if (s[i] == '\0') {
            break;
        }
        (*len) -= i;
        return &s[i];
    }

    (*len) = 0;
    return NULL;
}

const char *skip_tail_space(const char *s, int *len)
{
    int i;

    tdom_assert(s != NULL);
    tdom_assert(len != NULL);
    tdom_assert((*len) >= 0);

    for (i = (*len) - 1; i >= 0; i--) {
        if (TDOM_IS_SPACE(s[i]) || TDOM_IS_CRLF(s[i]) || s[i] == '\0') {
            (*len)--;
            continue;
        }
        break;
    }

    return (*len) > 0 ? s : NULL;
}

int cut_tail_space(struct tdom_text *text)
{
    int len;
    char *value;

    if (text == NULL || text->value == NULL) {
        return 1;
    }

    tdom_assert(text->node.type == TDOM_TEXT);

    len = strlen(text->value);
    skip_tail_space(text->value, &len);
    tdom_assert(len >= 0);

    if ((value = malloc(len + 1)) == NULL) {
        return -2;
    }
    snprintf(value, len + 1, "%s", text->value);
    value[len] = '\0';
    free(text->value);
    text->value = value;

    return 0;
}

int tdom_count_escaped_len(const char *value, int len)
{
    int sum = 0;
    int i;

    tdom_assert(value != NULL);

    for (i = 0; i < len; i++) {
        if (value[i] == '<' || value[i] == '>') {         /* &lt; &gt; */
            sum += 4;
        } else if (value[i] == '&') {                     /* &amp; */
            sum += 5;
        } else if (value[i] == '"' || value[i] == '\'') { /* &quot; &apos; */
            sum += 6;
        } else {
            sum += 1;
        }
    }

    return sum;
}

int tdom_escape(const char *value, int len, char *buffer, int *size)
{
    int sum = 0;
    int i;
    const char *p;
    int n;
    int readsize = 0;

    tdom_assert(value);
    tdom_assert(buffer);

    for (i = 0; i < len; i++) {
        if (value[i] == '<') {
            p = "&lt;";
            n = 4;
        } else if (value[i] == '>') {
            p = "&gt;";
            n = 4;
        } else if (value[i] == '&') {
            p = "&amp;";
            n = 5;
        } else if (value[i] == '"') {
            p = "&quot;";
            n = 6;
        } else if (value[i] == '\'') {
            p = "&apos;";
            n = 6;
        } else {
            if ((sum += 1) >= *size) {
                i++;
                break;
            }
            continue;
        }
        if (sum > 0) {
            sum = min(sum, *size);
            strncpy(buffer, &value[i  - sum], sum);
            buffer += sum;
            *size -= sum;
            readsize += sum;
            sum = 0;
        }
        if (*size < n) {
            return readsize;
        }
        strncpy(buffer, p, n);
        buffer += n;
        readsize++;
        if ((*size -= n) <= 0) {
            return readsize;
        }
    }

    if (sum > 0) {
        sum = min(sum, *size);
        strncpy(buffer, &value[i  - sum], sum);
        *size -= sum;
        readsize += sum;
    }

    return readsize;
}

int tdom_unescape(char *value, int *len)
{
    int n;
    char *rs = value;
    char *rc = value;
    char *w = value;
    char *end;

    tdom_assert(value);
    tdom_assert(len);
    tdom_assert(*len >= 0);

    end = value + *len;
    while (rc < end) {
        if (*rc != '&') {
            rc++;
            continue;
        }
        if ((n = rc - rs) > 0) {
            strncpy(w, rs, n);
            w += n;
            rs += n;
        }
        rc++;
        rs++;
        if ((rc + 5) <= end) {
            if (strncmp(rc, "quot;", 5) == 0) {
                *w = '"';
                n = 5;
            } else if (strncmp(rc, "apos;", 5) == 0) {
                *w = '\'';
                n = 5;
            } else if (strncmp(rc, "amp;", 4) == 0) {
                *w = '&';
                n = 4;
            } else if (strncmp(rc, "lt;", 3) == 0) {
                *w = '<';
                n = 3;
            } else if (strncmp(rc, "gt;", 3) == 0) {
                *w = '>';
                n = 3;
            } else {
                *w = '&';
                n = 0;
            }
            w++;
            rs += n;
            rc += n;
            continue;
        } else if ((rc + 4) <= end) {
            if (strncmp(rc, "amp;", 4) == 0) {
                *w = '&';
                n = 4;
            } else if (strncmp(rc, "lt;", 3) == 0) {
                *w = '<';
                n = 3;
            } else if (strncmp(rc, "gt;", 3) == 0) {
                *w = '>';
                n = 3;
            } else if (strncmp(rc, "apos", 4) == 0 ||
                       strncmp(rc, "quot", 4) == 0)
            {
                strncpy(w, &rc[-1], 5);
                *len = w - value + 5;
                return 5;
            } else {
                *w = '&';
                n = 0;
            }
            w++;
            rs += n;
            rc += n;
            continue;
        } else if ((rc + 3) <= end) {
            if (strncmp(rc, "lt;", 3) == 0) {
                *w = '<';
                n = 3;
            } else if (strncmp(rc, "gt;", 3) == 0) {
                *w = '>';
                n = 3;
            } else if (strncmp(rc, "apo", 3) == 0 ||
                       strncmp(rc, "quo", 3) == 0 ||
                       strncmp(rc, "amp", 3) == 0)
            {
                strncpy(w, &rc[-1], 4);
                *len = w - value + 4;
                return 4;
            } else {
                *w = '&';
                n = 0;
            }
            w++;
            rs += n;
            rc += n;
            continue;
        } else if ((rc + 2) <= end) {
            if (strncmp(rc, "gt", 2) == 0 || strncmp(rc, "lt", 2) == 0 ||
                strncmp(rc, "ap", 2) == 0 || strncmp(rc, "qu", 2) == 0 ||
                strncmp(rc, "am", 2) == 0)
            {
                strncpy(w, &rc[-1], 3);
                *len = w - value + 3;
                return 3;
            } else {
                *w = '&';
                n = 0;
            }
            w++;
            rs += n;
            rc += n;
            continue;
        } else if ((rc + 1) <= end) {
            if (*rc == 'l' || *rc == 'g' || *rc == 'a' || *rc == 'q') {
                strncpy(w, &rc[-1], 2);
                *len = w - value + 2;
                return 2;
            } else {
                *w = '&';
                n = 0;
            }
            w++;
            rs += n;
            rc += n;
            continue;
        } else {
            *w = '&';
            *len = w - value + 1;
            return 1;
        }
    }

    if ((n = rc - rs) > 0) {
        strncpy(w, rs, n);
        w += n;
    }
    *len = w - value;

    return 0;
}

struct tdom_doc *tdom_soap_new(const char *service_type,
                               const char *action)
{
    struct tdom_doc *doc = NULL;
    struct tdom_elem *elem = NULL;
    struct tdom_elem *body;
    char *name = NULL;
    int len;

    if (service_type == NULL || action == NULL) {
        return NULL;
    }

    len = 2 + strlen(action);
    if ((name = malloc(len + 1)) == NULL) {
        goto out;
    }
    snprintf(name, len + 1, "u:%s", action);
    name[len] = '\0';

    if ((doc = tdom_doc_new(NULL, "1.0", NULL)) == NULL) {
        goto out;
    }

    if ((elem = tdom_elem_new(NULL, "s:Envelope")) == NULL) {
        goto out;
    }
    if (tdom_set_ns(elem, "s", TDOM_SOAP_ENV_NS_URI) < 0)
    {
        goto out;
    }
    if (tdom_set_attr(elem, "s:encodingStyle", TDOM_SOAP_ENC_NS_URI, NULL) < 0)
    {
        goto out;
    }
    doc->root = elem;

    if ((elem = tdom_elem_new(NULL, "s:Body")) == NULL) {
        goto out;
    }
    tdom_append_child(doc->root, &elem->node);
    body = elem;

    if ((elem = tdom_elem_new(NULL, name)) == NULL) {
        goto out;
    }
    if (tdom_set_ns(elem, "u", service_type) < 0) {
        goto out;
    }
    tdom_append_child(body, &elem->node);

    free(name);
    return doc;

out:
    tdom_elem_delete(elem);
    tdom_doc_delete(doc);
    free(name);
    return NULL;
}

int tdom_soap_set_pointer(struct tdom_doc *doc, const char *argname, char *value)
{
    struct tdom_elem *parent;
    struct tdom_elem *elem = NULL;
    struct tdom_elem *old;
    struct tdom_text *text = NULL;

    if (doc == NULL || argname == NULL || value == NULL) {
        return -1;
    }

    /* get s:Body elem */
    parent = tdom_get_elem(doc->root, "Body", TDOM_SOAP_ENV_NS_URI, 0);
    if (parent == NULL) {
        return -1;
    }

    /* get action elem */
    parent = (struct tdom_elem *)TAILQ_FIRST(&parent->children);
    if (parent == NULL) {
        return -1;
    }

    /* new arg elem */
    if ((elem = tdom_elem_new(NULL, argname)) == NULL) {
        goto out;
    }
    if ((text = tdom_text_new(NULL, NULL)) == NULL) {
        goto out;
    }
    text->value = value;

    /* remove old if any */
    if ((old = tdom_get_elem(parent, argname, NULL, 0)) != NULL) {
        TAILQ_REMOVE(&parent->children, &old->node, sibling);
        tdom_elem_delete(old);
    }

    tdom_append_child(elem, &text->node);
    tdom_append_child(parent, &elem->node);

    return 0;

out:
    tdom_text_delete(text);
    tdom_elem_delete(elem);
    return -2;
}

int tdom_soap_set(struct tdom_doc *doc, const char *argname, const char *value)
{
    int ret;
    char *p;
    int len;

    if (value == NULL) {
        return -1;
    }

    len = strlen(value);
    if ((p = malloc(len + 1)) == NULL) {
        return -2;
    }
    snprintf(p, len + 1, "%s", value);
    p[len] = '\0';

    if ((ret = tdom_soap_set_pointer(doc, argname, p)) < 0) {
        free(p);
        return ret;
    }

    return 0;
}

const char *tdom_soap_get(const struct tdom_doc *doc, const char *argname)
{
    struct tdom_elem *parent;

    if (doc == NULL || argname == NULL) {
        return NULL;
    }

    /* get s:Body elem */
    parent = tdom_get_elem(doc->root, "Body", TDOM_SOAP_ENV_NS_URI, 0);
    if (parent == NULL) {
        return NULL;
    }

    /* get action elem */
    parent = (struct tdom_elem *)TAILQ_FIRST(&parent->children);

    return tdom_get_value(tdom_get_elem(parent, argname, NULL, 0));
}

struct __elem {
    const char *name;
    const char *ns;
    const char *value;
    int parent;
};

struct tdom_doc *tdom_soap_err_new(int error_code, const char *error_string)
{
    struct tdom_doc *doc = NULL;
    struct tdom_elem *elem = NULL;
    struct tdom_elem *parent;
    char buff[16];
    int i;
    struct __elem elems[] = {
        {"s:Body",           NULL, NULL,         1},
        {"s:Fault",          NULL, NULL,         1},
        {"faultcode",        NULL, "s:Client",   0},
        {"faultstring",      NULL, "UPnPError",  0},
        {"detail",           NULL, NULL,         1},
        {"UPnPError",        TDOM_NS_UPNP_CONTROL, NULL, 1},
        {"errorCode",        NULL, buff,         0},
        {"errorDescription", NULL, error_string, 0},
    };

    snprintf(buff, sizeof(buff), "%d", error_code);
    buff[sizeof(buff) - 1] = '\0';

    if ((doc = tdom_doc_new(NULL, "1.0", NULL)) == NULL) {
        goto out;
    }

    if ((elem = tdom_elem_new(NULL, "s:Envelope")) == NULL) {
        goto out;
    }
    if (tdom_set_ns(elem, "s", TDOM_SOAP_ENV_NS_URI) < 0)
    {
        goto out;
    }
    if (tdom_set_attr(elem, "s:encodingStyle", TDOM_SOAP_ENC_NS_URI, NULL) < 0)
    {
        goto out;
    }
    doc->root = elem;
    parent = elem;

    for (i = 0; i < (sizeof(elems)/sizeof(*elems)); i++) {
        if ((elem = tdom_elem_new(NULL, elems[i].name)) == NULL)
            goto out;
        if (elems[i].ns && tdom_set_ns(elem, "", elems[i].ns) < 0)
            goto out;
        tdom_append_child(parent, &elem->node);
        if (elems[i].value && tdom_set_value(elem, elems[i].value) < 0)
            goto out;
        if (elems[i].parent)
            parent = elem;
    }

    return doc;

out:
    tdom_elem_delete(elem);
    tdom_doc_delete(doc);
    return NULL;
}

const char *tdom_soap_get_upnp_err(const struct tdom_doc *doc, const char *name)
{
    struct tdom_elem *elem;

    if (doc == NULL || name == NULL) {
        return NULL;
    }

    elem = tdom_get_elem(doc->root, "Body", TDOM_SOAP_ENV_NS_URI, 0);
    elem = tdom_get_elem(elem, "Fault", TDOM_SOAP_ENV_NS_URI, 0);
    elem = tdom_get_elem(elem, "detail", NULL, 0);
    elem = tdom_get_elem(elem, "UPnPError", TDOM_NS_UPNP_CONTROL, 0);
    elem = tdom_get_elem(elem, name, TDOM_NS_UPNP_CONTROL, 0);

    return tdom_get_value(elem);
}

